OptionsBasedMetricRecordingEnricher Class

Namespace: Diginsight.Diagnostics
Assembly: Diginsight.Diagnostics.dll

Configuration-based enricher that adds contextual tags to metrics by extracting specified tag names from activity hierarchies.

public class OptionsBasedMetricRecordingEnricher : IMetricRecordingEnricher

Inheritance

Object ? OptionsBasedMetricRecordingEnricher

Implements

  • IMetricRecordingEnricher

Summary

The OptionsBasedMetricRecordingEnricher class automatically extracts business-relevant tags from activities and their parent hierarchy to enrich metrics with contextual dimensions. It uses a declarative, configuration-driven approach where you specify tag names in appsettings.json, and the enricher searches the activity tree to find matching tag values.

Key capabilities: - ? Configuration-driven enrichment - specify tag names in appsettings.json - ? Hierarchical tag search - searches activity and all parent activities - ? Instrument-specific tags - different tags per metric instrument - ? Tag deduplication - combined instrument-specific and general tags with duplicates removed - ? Null-safe extraction - only includes tags with non-null values - ? Virtualizable method - easily extensible for custom enrichment logic - ? Low cardinality support - promotes using categorical tags from configuration


Constructors

OptionsBasedMetricRecordingEnricher(IOptionsMonitor)

Initializes a new instance of the OptionsBasedMetricRecordingEnricher class.

public OptionsBasedMetricRecordingEnricher(
    IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions> enricherMonitor
)

Parameters

enricherMonitor : IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions>
Monitor for accessing enricher configuration options. Uses IOptionsMonitor to support dynamic configuration changes at runtime without restarting the application.

Remarks

The constructor stores the options monitor which allows: - Access to named options for instrument-specific tag configurations - Hot reload support - configuration changes apply immediately - Default configuration via CurrentValue property


Methods

ExtractTags(Activity, Instrument)

Extracts tags from the activity hierarchy based on configured tag names.

public virtual Tags ExtractTags(Activity activity, Instrument instrument)

Returns

Tags (which is IEnumerable<Tag>)
A collection of tags where each tag’s value was found in the activity hierarchy. Only includes tags with non-null values.

Parameters

activity : Activity
The activity from which to extract tags. The enricher searches this activity and all parent activities.

instrument : Instrument
The metric instrument that will use these tags (e.g., histogram for span duration).

Remarks

This method implements a sophisticated tag extraction strategy:

Two-tier configuration: 1. Instrument-specific tags: First retrieves tag names from configuration named by instrument.Name 2. General tags: Then retrieves tag names from default (unnamed) configuration 3. Combines and deduplicates: Merges both lists and removes duplicate tag names

Hierarchical search: For each configured tag name, the enricher: 1. Searches the current activity’s tags 2. If not found, searches each parent activity moving up the hierarchy 3. Returns the first non-null value found

Implementation logic:

// Step 1: Get tag names from instrument-specific and general config
var instrumentTags = GetTagNames(enricherMonitor.Get(instrument.Name));
var generalTags = GetTagNames(enricherMonitor.CurrentValue);
var allTagNames = instrumentTags.Concat(generalTags).Distinct();

// Step 2: For each tag name, search activity hierarchy
foreach (var tagName in allTagNames)
{
    // Search current activity and all parents
    var value = activity.GetAncestors(includeSelf: true)
        .Select(a => a.GetTagItem(tagName))
        .FirstOrDefault(v => v != null);
    
    if (value != null)
        yield return new Tag(tagName, value);
}

Null handling: - If a tag name doesn’t exist in any activity in the hierarchy, it’s skipped - Only tags with non-null values are included in the result

Example

Configuration:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "customer_tier",
      "region",
      "environment"
    ]
  },
  "diginsight.span_duration": {
    "MetricTags": [
      "operation_category",
      "customer_tier"
    ]
  }
}

Activity hierarchy:

// Root activity
using var rootActivity = activitySource.StartRichActivity("HandleRequest", new
{
    customer_tier = "premium",
    region = "us-east",
    environment = "production"
});

// Child activity
using var childActivity = activitySource.StartRichActivity("ProcessOrder", new
{
    operation_category = "order",
    order_id = "12345"  // Not configured, won't be extracted
});

// For instrument "diginsight.span_duration"
var tags = enricher.ExtractTags(childActivity, spanDurationInstrument);
// Result: [
//   Tag("operation_category", "order"),        // From child (instrument-specific)
//   Tag("customer_tier", "premium"),           // From parent (both configs, deduplicated)
//   Tag("region", "us-east"),                  // From parent (general config)
//   Tag("environment", "production")           // From parent (general config)
// ]

Search order example:

// Looking for "customer_tier" tag
// 1. Check childActivity.GetTagItem("customer_tier") ? null
// 2. Check rootActivity.GetTagItem("customer_tier") ? "premium" ?
// Result: Tag("customer_tier", "premium")

Configuration

OptionsBasedMetricRecordingEnricherOptions

Configure the enricher in appsettings.json:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "<tag_name_1>",
      "<tag_name_2>"
    ]
  }
}

Properties

MetricTags : ICollection<string>
List of tag names to extract from activities and include in metrics.

Guidelines: - ? Use low-cardinality tags (status, tier, region, category) - ? Use business-relevant dimensions for filtering and grouping - ? Avoid high-cardinality identifiers (user IDs, order IDs, request IDs) - ? Avoid unbounded values (URLs, timestamps, freeform text)

Named Configuration (Instrument-Specific)

Configure different tags for specific metric instruments:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "environment",
      "region",
      "customer_tier"
    ]
  },
  "diginsight.span_duration": {
    "MetricTags": [
      "operation_category",
      "criticality_level"
    ]
  },
  "http.server.request.duration": {
    "MetricTags": [
      "endpoint_category",
      "api_version"
    ]
  }
}

How it works: - Section name matches instrument.Name parameter - Instrument-specific tags are combined with general tags - Duplicate tag names are automatically removed - All tags are searched in the activity hierarchy

Resulting tag names:

// For "diginsight.span_duration" instrument:
// ["operation_category", "criticality_level", "environment", "region", "customer_tier"]
// (instrument-specific + general, deduplicated)

// For "http.server.request.duration" instrument:
// ["endpoint_category", "api_version", "environment", "region", "customer_tier"]

// For other instruments:
// ["environment", "region", "customer_tier"]
// (only general tags)

Usage Examples

Basic Registration

Register the enricher during application startup:

var builder = WebApplication.CreateBuilder(args);

// Register enricher
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();

// Register metric recorder (uses the enricher)
builder.Services.AddSpanDurationMetricRecorder();

var app = builder.Build();
app.Run();

Simple Configuration

appsettings.json:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "environment",
      "service_version",
      "deployment_slot"
    ]
  }
}

Application code:

using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
    environment = "production",
    service_version = "2.5.1",
    deployment_slot = "blue",
    order_id = "12345"  // Not in config, won't be extracted
});

// Resulting metric:
// diginsight.span_duration{
//   span_name="ProcessOrder",
//   status="Ok",
//   environment="production",
//   service_version="2.5.1",
//   deployment_slot="blue"
// } = 250ms

Business Context Tags

appsettings.json:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "customer_tier",
      "region",
      "feature_flag_set",
      "tenant_id"
    ]
  }
}

Application code:

public class OrderController : ControllerBase
{
    [HttpPost("orders")]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        // Root activity with business context
        using var activity = activitySource.StartRichActivity("CreateOrder", new
        {
            customer_tier = GetCustomerTier(request.CustomerId),
            region = request.ShippingAddress.Region,
            feature_flag_set = featureFlags.GetActiveSet(),
            tenant_id = GetTenantId()
        });
        
        var order = await orderService.ProcessOrderAsync(request);
        return Ok(order);
    }
}

// Child activities automatically inherit parent tags
public async Task ProcessOrderAsync(CreateOrderRequest request)
{
    // This activity doesn't set any tags
    using var activity = activitySource.StartMethodActivity();
    
    // But metrics still include parent tags:
    // diginsight.span_duration{
    //   span_name="ProcessOrderAsync",
    //   customer_tier="premium",
    //   region="us-east",
    //   feature_flag_set="v2_checkout",
    //   tenant_id="tenant-abc"
    // }
    
    await ValidateInventory();
    await ProcessPayment();
}

Hierarchical Tag Resolution

Configuration:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "customer_tier",
      "operation_type",
      "correlation_id"
    ]
  }
}

Activity hierarchy:

// Root activity (e.g., from HTTP request middleware)
using var httpActivity = activitySource.StartRichActivity("HttpRequest", new
{
    correlation_id = Guid.NewGuid().ToString(),
    customer_tier = "enterprise"
});

// Business logic activity (overrides operation_type)
using var businessActivity = activitySource.StartRichActivity("ProcessOrder", new
{
    operation_type = "write"
});

// Database activity (inherits from ancestors)
using var dbActivity = activitySource.StartRichActivity("SaveOrder", new
{
    // No tags set
});

// Tags extracted for dbActivity:
// correlation_id ? searches dbActivity (null), businessActivity (null), httpActivity ("guid") ?
// customer_tier ? searches dbActivity (null), businessActivity (null), httpActivity ("enterprise") ?
// operation_type ? searches dbActivity (null), businessActivity ("write") ?
// Result: All three tags found in ancestor hierarchy

Instrument-Specific Tags

Different tags for different metric types:

appsettings.json:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "environment",
      "region"
    ]
  },
  "diginsight.span_duration": {
    "MetricTags": [
      "operation_category",
      "performance_tier"
    ]
  },
  "custom.database.query_cost": {
    "MetricTags": [
      "database_name",
      "collection_name",
      "partition_key"
    ]
  }
}

Application code:

using var activity = activitySource.StartRichActivity("ExecuteQuery", new
{
    environment = "production",
    region = "us-east",
    operation_category = "database",
    performance_tier = "premium",
    database_name = "orders",
    collection_name = "orders_2024",
    partition_key = "tenant-abc"
});

// For span_duration instrument:
// Tags: environment, region, operation_category, performance_tier

// For database.query_cost instrument:
// Tags: environment, region, database_name, collection_name, partition_key

// For other instruments:
// Tags: environment, region

Cardinality Control

Use enricher to promote low-cardinality tagging:

? Bad - High cardinality tags in activity:

using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
    order_id = "order-12345",        // Millions of unique values
    customer_id = "cust-67890",      // Millions of unique values
    request_id = Guid.NewGuid()      // Unlimited unique values
});
// These high-cardinality values are available for logging/tracing
// but won't become metric tags (not in config)

? Good - Low cardinality tags:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "customer_tier",
      "order_size_bucket",
      "payment_method"
    ]
  }
}
using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
    // High cardinality - available for logs/traces, not metrics
    order_id = "order-12345",
    customer_id = "cust-67890",
    
    // Low cardinality - will become metric tags
    customer_tier = "premium",           // 3 values: free, standard, premium
    order_size_bucket = "large",         // 4 values: small, medium, large, xlarge
    payment_method = "credit_card"       // 5 values: credit_card, paypal, invoice, etc.
});

// Resulting metric:
// diginsight.span_duration{
//   customer_tier="premium",
//   order_size_bucket="large",
//   payment_method="credit_card"
// } = 250ms
// Only ~60 unique combinations (3 � 4 � 5) - excellent cardinality!

Multiple Enrichers

Combine with custom enrichers for advanced scenarios:

builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, DeploymentContextEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, PerformanceTierEnricher>();

Custom enricher example:

public class DeploymentContextEnricher : IMetricRecordingEnricher
{
    public Tags ExtractTags(Activity activity, Instrument instrument)
    {
        return
        [
            new Tag("host", Environment.MachineName),
            new Tag("k8s_pod", Environment.GetEnvironmentVariable("POD_NAME")),
            new Tag("deployment_version", GetAssemblyVersion())
        ];
    }
}

Hot Reload Configuration

Changes to appsettings.json apply immediately without restart:

Initial configuration:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": ["environment", "region"]
  }
}

After hot reload (add more tags):

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "environment",
      "region",
      "customer_tier",
      "feature_flags"
    ]
  }
}

Result: - New activities immediately include the additional tags - No application restart required - Existing in-flight activities use old configuration

Custom Derived Enricher

Extend for custom logic:

public class SmartMetricEnricher : OptionsBasedMetricRecordingEnricher
{
    private readonly IFeatureFlagService _featureFlags;
    
    public SmartMetricEnricher(
        IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions> enricherMonitor,
        IFeatureFlagService featureFlags)
        : base(enricherMonitor)
    {
        _featureFlags = featureFlags;
    }
    
    public override Tags ExtractTags(Activity activity, Instrument instrument)
    {
        // Start with configuration-based tags
        var baseTags = base.ExtractTags(activity, instrument).ToList();
        
        // Add computed tags
        baseTags.Add(new Tag("ab_test_variant", _featureFlags.GetVariant("checkout_flow")));
        
        // Add conditional tags
        if (activity.Duration > TimeSpan.FromSeconds(1))
        {
            baseTags.Add(new Tag("slow_operation", "true"));
        }
        
        return baseTags;
    }
}

Registration:

builder.Services.AddSingleton<IMetricRecordingEnricher, SmartMetricEnricher>();

Tag Extraction Details

Activity Tag Lookup

Tags are extracted using Activity.GetTagItem(string key):

// Activity tags set during creation
using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
    customer_tier = "premium",    // Becomes tag: customer_tier = "premium"
    order_value = 1299.99,        // Becomes tag: order_value = 1299.99
    is_express = true             // Becomes tag: is_express = true
});

// Can also be set later
activity.SetTag("retry_count", 0);
activity.SetTag("cache_hit", true);

Hierarchical Search Process

Example hierarchy:

HttpRequest (root)
  ?? customer_id = "cust-123"
  ?? region = "us-east"
  ?? ProcessOrder
      ?? operation_type = "write"
      ?? SaveToDatabase
          ?? database = "orders"
          ?? (current activity)

Tag extraction for “customer_id”:

activity.GetAncestors(includeSelf: true)
// Returns: [SaveToDatabase, ProcessOrder, HttpRequest]

.Select(a => a.GetTagItem("customer_id"))
// Returns: [null, null, "cust-123"]

.FirstOrDefault(v => v != null)
// Returns: "cust-123"

Search is efficient: - Stops at first non-null value (early exit) - No unnecessary hierarchy traversal - Lazy evaluation with LINQ

Value Type Support

Tags can have various value types:

using var activity = activitySource.StartRichActivity("Operation", new
{
    string_tag = "value",           // string
    int_tag = 42,                   // int
    double_tag = 3.14,              // double
    bool_tag = true,                // bool
    enum_tag = HttpStatusCode.OK,   // enum
    guid_tag = Guid.NewGuid()       // Guid
});

// All types are preserved in Tag structure
// Metric exporters handle type conversion as needed

Type handling by exporters: - Prometheus: Converts to strings - Application Insights: Preserves types as custom dimensions - OTLP: Supports typed attributes per OpenTelemetry spec


Performance Considerations

Configuration Freezing

Immutability for thread safety:

((IOptionsBasedMetricRecordingEnricherOptions)options.Freeze())
  • Options are frozen to immutable collections before use
  • Prevents modification during concurrent access
  • Creates immutable list copy only once per instrument

LINQ Pipeline Efficiency

Deduplication strategy:

instrumentTags.Concat(generalTags).Distinct()
  • Combines instrument-specific and general tag names
  • Distinct() removes duplicate names efficiently (hash-based)
  • Minimal allocations with deferred execution

Hierarchical search:

activity.GetAncestors(includeSelf: true)
    .Select(a => a.GetTagItem(tagName))
    .FirstOrDefault(v => v != null)
  • FirstOrDefault with predicate enables early exit
  • Stops at first non-null value
  • No unnecessary parent traversal

Memory Efficiency

Tag collection: - Returns IEnumerable<Tag> with deferred execution - Only materializes tags that exist in hierarchy - No allocations for missing tags

Best practices:

// ? Good - minimal tag list
"MetricTags": ["environment", "region", "tier"]

// ? Bad - excessive tags
"MetricTags": [ /* 20+ tag names */ ]

Thread Safety

The OptionsBasedMetricRecordingEnricher is thread-safe:

  • ? IOptionsMonitor<T> is thread-safe by design
  • ? Freeze() creates immutable snapshot for evaluation
  • ? No mutable state between enrichment operations
  • ? LINQ operations are stateless
  • ? Activity tag lookup is thread-safe

Multiple threads can enrich different activities concurrently without synchronization issues.


Troubleshooting

Tags Not Appearing in Metrics

Symptoms: Expected tags don’t show up in metrics.

Checklist:

  1. Is the enricher registered?

    builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
  2. Is the configuration section correctly named?

    "OptionsBasedMetricRecordingEnricher": {
      "MetricTags": [ ... ]
    }
  3. Are tag names spelled correctly?

    // Activity tag name must match config exactly
    using var activity = activitySource.StartRichActivity("Op", new
    {
        customer_tier = "premium"  // Must match "customer_tier" in config
    });
  4. Are tags set on the activity?

    // Debug: check what tags exist
    Console.WriteLine($"Tags: {string.Join(", ", activity.Tags.Select(t => $"{t.Key}={t.Value}"))}");
  5. Is the tag value null?

    // Null values are excluded
    using var activity = activitySource.StartRichActivity("Op", new
    {
        customer_tier = (string?)null  // Won't appear in metrics
    });

Tags Not Found in Hierarchy

Symptoms: Tags exist but aren’t extracted.

Cause: Tag set on activity after enrichment occurs.

Example:

using var activity = activitySource.StartActivity("Op");
// Enrichment happens here (activity stopping)

// ? Too late - enrichment already occurred
activity?.SetTag("customer_tier", "premium");

Solution: Set tags during activity creation:

// ? Correct - tags available for enrichment
using var activity = activitySource.StartRichActivity("Op", new
{
    customer_tier = "premium"
});

Hierarchical Search Not Working

Symptoms: Parent tags not inherited by child activities.

Debugging:

public Tags ExtractTags(Activity activity, Instrument instrument)
{
    // Debug: print hierarchy
    foreach (var ancestor in activity.GetAncestors(includeSelf: true))
    {
        Console.WriteLine($"Activity: {ancestor.OperationName}");
        foreach (var tag in ancestor.Tags)
        {
            Console.WriteLine($"  {tag.Key} = {tag.Value}");
        }
    }
    
    return base.ExtractTags(activity, instrument);
}

Common issue: Activity hierarchy broken

// ? Wrong - creates unrelated activity
using var activity1 = activitySource.StartActivity("Parent");
using var activity2 = activitySource.StartActivity("Child");
// activity2.Parent != activity1 (both are children of current Activity)

// ? Correct - proper hierarchy
using var activity1 = activitySource.StartActivity("Parent");
// activity1 is now Activity.Current
using var activity2 = activitySource.StartActivity("Child");
// activity2.Parent == activity1 ?

High Cardinality Issues

Symptoms: Excessive storage costs, slow queries.

Cause: Too many unique tag combinations.

Diagnosis:

// Check configured tags
var options = serviceProvider.GetService<IOptions<OptionsBasedMetricRecordingEnricherOptions>>();
Console.WriteLine($"Configured tags: {string.Join(", ", options.Value.MetricTags)}");

// Estimate cardinality
// If customer_id is configured: ? Millions of unique values
// If customer_tier is configured: ? ~3 unique values

Solution: Review configured tags for cardinality:

{
  "MetricTags": [
    "customer_id"        // ? Remove - high cardinality
    "customer_tier",     // ? Keep - low cardinality (3 values)
    "region",            // ? Keep - low cardinality (~10 values)
    "request_path"       // ? Remove - high cardinality (thousands)
    "endpoint_category"  // ? Keep - low cardinality (5 values)
  ]
}

Performance Impact

Symptoms: Increased latency or CPU usage.

Diagnosis:

// Check enricher overhead
var stopwatch = Stopwatch.StartNew();
var tags = enricher.ExtractTags(activity, instrument).ToArray();
stopwatch.Stop();
Console.WriteLine($"Enrichment took: {stopwatch.ElapsedMilliseconds}ms");
Console.WriteLine($"Extracted {tags.Length} tags");

Mitigation: - Reduce configured tag count (< 10 tags) - Limit activity hierarchy depth (< 10 levels) - Profile hierarchical search for bottlenecks - Consider caching for repeated extractions


Design Patterns

Strategy Pattern

The enricher implements the Strategy pattern: - IMetricRecordingEnricher defines the enrichment strategy interface - OptionsBasedMetricRecordingEnricher is one concrete strategy - SpanDurationMetricRecorder uses enrichers without knowing implementation details

Options Pattern

Uses the .NET Options pattern: - IOptionsMonitor<T> for reactive configuration - Named options for instrument-specific configuration - Hot reload support without restart

Chain of Responsibility

Multiple enrichers can be registered:

// Each enricher contributes tags
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, DeploymentEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, BusinessEnricher>();

// SpanDurationMetricRecorder collects tags from all enrichers
var allTags = enrichers.SelectMany(e => e.ExtractTags(activity, instrument));

Template Method Pattern

Virtual ExtractTags method enables customization:

public class CustomEnricher : OptionsBasedMetricRecordingEnricher
{
    public override Tags ExtractTags(Activity activity, Instrument instrument)
    {
        // Custom pre-processing
        var baseTags = base.ExtractTags(activity, instrument).ToList();
        
        // Add additional tags
        baseTags.Add(new Tag("custom", "value"));
        
        return baseTags;
    }
}

Best Practices

Tag Selection

? DO choose low-cardinality business dimensions:

{
  "MetricTags": [
    "customer_tier",        // 3-5 values
    "region",               // ~10 values
    "operation_category",   // 5-10 values
    "deployment_ring",      // 3-4 values
    "feature_flag_variant"  // 2-3 values per flag
  ]
}

? DON’T include high-cardinality identifiers:

{
  "MetricTags": [
    "customer_id",      // ? Millions of values
    "order_id",         // ? Unlimited values
    "request_id",       // ? Unlimited values
    "user_agent",       // ? Thousands of values
    "url_path"          // ? Thousands of values
  ]
}

Configuration Organization

? DO group by metric purpose:

{
  "OptionsBasedMetricRecordingEnricher": {
    "MetricTags": [
      "environment",
      "region",
      "deployment_slot"
    ]
  },
  "diginsight.span_duration": {
    "MetricTags": [
      "operation_category",
      "performance_tier"
    ]
  },
  "http.server.request.duration": {
    "MetricTags": [
      "endpoint_category",
      "api_version"
    ]
  }
}

? DO use consistent tag naming:

{
  "MetricTags": [
    "customer_tier",        // ? snake_case, descriptive
    "deployment_region",    // ? fully qualified
    "api_version"           // ? clear meaning
  ]
}

? DON’T use inconsistent naming:

{
  "MetricTags": [
    "customerTier",    // ? Mixed case
    "region",          // ? Ambiguous (deployment? customer?)
    "v"                // ? Unclear abbreviation
  ]
}

Activity Tagging

? DO set tags during activity creation:

using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
    customer_tier = GetCustomerTier(),
    region = GetRegion(),
    operation_category = "order_processing"
});

? DO set business context at root activity:

// HTTP middleware or entry point
using var rootActivity = activitySource.StartRichActivity("HandleRequest", new
{
    customer_tier = context.GetCustomerTier(),
    region = context.GetRegion(),
    tenant_id = context.GetTenantId()
});

// Child activities inherit these automatically

? DON’T duplicate tags on every child:

// ? Wasteful - region already set on parent
using var childActivity = activitySource.StartRichActivity("SubOperation", new
{
    region = GetRegion()  // Unnecessary duplication
});

Testing Enrichers

[Fact]
public void ExtractTags_RetrievesFromHierarchy()
{
    // Arrange
    var options = new OptionsBasedMetricRecordingEnricherOptions
    {
        MetricTags = { "customer_tier", "region" }
    };
    var monitor = Mock.Of<IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions>>(
        m => m.CurrentValue == options &&
             m.Get(It.IsAny<string>()) == new OptionsBasedMetricRecordingEnricherOptions());
    
    var enricher = new OptionsBasedMetricRecordingEnricher(monitor);
    
    var parentSource = new ActivitySource("Parent");
    var childSource = new ActivitySource("Child");
    
    using var parent = parentSource.StartActivity("ParentOp")!;
    parent.SetTag("customer_tier", "premium");
    parent.SetTag("region", "us-east");
    
    using var child = childSource.StartActivity("ChildOp")!;
    // child doesn't have tags
    
    var instrument = Mock.Of<Instrument>(i => i.Name == "test.metric");
    
    // Act
    var tags = enricher.ExtractTags(child, instrument).ToArray();
    
    // Assert
    Assert.Contains(tags, t => t.Key == "customer_tier" && t.Value.Equals("premium"));
    Assert.Contains(tags, t => t.Key == "region" && t.Value.Equals("us-east"));
}

Version History

Version Changes
3.0.0 Initial release with IMetricRecordingEnricher support
3.1.0 Added instrument-specific named configuration support
3.2.0 Improved hierarchical tag resolution performance

See Also


Remarks

The OptionsBasedMetricRecordingEnricher provides a declarative approach to metric enrichment that promotes low-cardinality tagging through configuration. By searching the activity hierarchy, it enables setting business context once at the root activity level and automatically propagating it to all child operations’ metrics.

Design principles: - ?? Configuration over code - define tags in settings, not scattered through code - ?? Low cardinality by design - configuration makes high-cardinality tags obvious - ?? Hierarchical context - set once at root, inherit in children - ? Performance-optimized - early exit search with frozen options - ?? Hot reload - configuration changes apply immediately - ?? Composable - works alongside custom enrichers

Common use cases: - Add deployment context (environment, region, version) to all metrics - Include business dimensions (customer tier, tenant) for filtering - Tag with feature flags for A/B test analysis - Add operation categories for grouping and alerting - Include performance tiers for SLA monitoring

Integration:

builder.Services
    .AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>()
    .AddSpanDurationMetricRecorder();

This configuration-based approach ensures that metric dimensions remain consistent, maintainable, and observable through version control, making it easier to reason about telemetry costs and cardinality in production environments.

Back to top